# Copyright 2009 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

""" Code to provide cross-request persistent storage for a given user.

The following classes are exported:
  SessionProvider: Acts as a dict to store data for a user across many requests.
"""

# Python imports
import random
import logging
import time
import hashlib
import pickle

# App Engine imports
from google.appengine.ext import db

# Local imports
import controller
import settings
import utils 

class SessionProvider(dict):
  """ Acts as a dict to store data for a user across many requests.
  
  All session data is stored as a dictionary written to memcache.  When the
  data times out of memcache, then the session is expired and all data
  will be lost.  Data cached as a property of this object and written to 
  memcache upon write, but not fetched from memcache upon read.  Theoretically
  this means that there could be multiple session provider objects instantiated
  for the same session overwriting each other's data, but the extra complexity
  of avoiding this was undesirable for what is essentially a sample about
  Friend Connect, and not how to write a session handler for App Engine.  A
  real production site should probably rely on an existing session provider
  instead of rolling their own- Django has an implementation which works on
  App Engine.
  
  When this object is garbage collected, it attempts to write its local data
  back into memcache.
  
  SessionProvider inherits from dict, so you can do the following:
      session = SessionProvider()
      session["key"] = "value"
      ...
      value = session["key"]
  """
  __key = None
  __killed = False
  
  def __new__(cls, key=None, auto_create=False):
    """ Gets a session with the corresponding key.
    
    Args:
      key: The key corresponding to the session to retrieve data for.
      auto_create: If this is set to True and no session is found with the 
          corresponding key, one will be created - used for creating sessions
          for Friend Connect users (since the key is generated by Friend 
          Connect, not by this provider).
    
    Returns:
      A SessionProvider or None if no session existed for the given key and
      auto_create was False.
    """
    session = None
    if key is None:
      key = SessionProvider.generate_session_key()
    else:
      data = utils.cache_get("session", key)
      if data is not None:
        session = pickle.loads(data)
        
    if not session:    
      session = dict.__new__(cls, {})
      session.__key = key
      session["started"] = time.time()
      
    return session
    
  def __init__(self, *args, **kwargs):
    """ Init function to override default dictionary init code. """
    pass
    
  def __del__(self):
    """ Writes the current data to the session when this object is collected.
    
    When the session provider is garbage collected, the session data which 
    is stored as a property on the object is written to the data store again.
    This is mostly to refresh the session timeout value, so that the session
    is deleted settings.SESSION_TIMEOUT seconds from its last access, not its
    last write.
    """
    try:
      self._write_session()
      logging.info("Session garbage collection cleanup succeeded")
    except Exception:
      logging.critical("Session garbage collection cleanup failed")

  def __setitem__(self, key, value):
    """ Stores an item in the session.
    
    Writes the entire session data to the memory cache.
    
    Args:
      key: The key under which to store this data.  
      value: The value to store.
    """
    super(SessionProvider, self).__setitem__(key, value)
    self._write_session()  
    
  def __getitem__(self, key):
    """ Gets an item from the session.
    
    Gets the data with the corresponding key from this object's local 
    dictionary.  Warning: does not check the memory cache to see if this value
    has been updated somewhere else.
    
    Args:
      key: The key corresponding to the data that is requested.
      
    Returns:
      The stored data or None if it does not exist.
    """
    try:
      return super(SessionProvider, self).__getitem__(key)
    except KeyError:
      return None

  def __delitem__(self, key):
    """ Removes an item from the session.
    
    Args:
      key: The key corresponding to the data to be removed.
    """
    super(SessionProvider, self).__delitem__(key)
    self._write_session()
    
  def _write_session(self):
    """ Writes a serialized version of this object to the memory cache.
    
    This method writes the serialized version of this class to the memory cache
    for settings.SESSION_TIMEOUT seconds from now, meaning that all session 
    data is cleared that many seconds from its last write.
    
    If the session has been marked as being "killed", no data will be written.
    """
    if not self.__killed:
      lifespan = settings.SESSION_TIMEOUT
      utils.cache_set(pickle.dumps(self), "session", self.key, time=lifespan)

  @property
  def key(self):
    """ Gets the key for this session.
    
    Returns:
      A string which may be used to create another SessionProvider with 
      access to the data stored in this object.
    """
    return self.__key
    
  def kill(self):
    """ Destroys a session.
    
    Calling this method will prevent any future memory cache storage of the 
    data in this object.  The data existing in the memory cache will be 
    deleted as well.
    """
    self.__killed = True
    logging.debug("Killing session: %s" % self.key)
    return utils.cache_delete("session", self.key)
    
  @staticmethod
  def generate_session_key():
    """ Creates a new session identifier which is not in use.
    
    Returns:
      A random sha1 hash that does not correspond to an existing session.
    """
    while True:
      random_number = random.randrange(0, 18446744073709551616L)
      hash_seed = "%s|%s" % (random_number, time.time())
      session_key = hashlib.sha1(hash_seed).hexdigest()
      if not SessionProvider.session_exists(session_key):
        return session_key

  @staticmethod
  def session_exists(session_key):
    """ Checks to see whether a session with the given key exists.
    
    Args:
      session_key: The string to check.
      
    Returns:
      True if a session exists with the given key, False otherwise.
    """
    return utils.cache_get("session", session_key) is not None
    
